#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
PT/BT下载器增强版 (适配 libtorrent 2.0.11)
功能：
1. 支持种子文件/磁力链接
2. PT站点passkey自动追加
3. 文件列表显示与选择下载
4. 指定文件范围下载
5. 实时进度显示
"""

import libtorrent as lt
import socket
import re
import time
import sys
import ssl

from urllib.parse import urlparse, urlunparse, parse_qs
from typing import Union, List, Dict

ssl._create_default_https_context = ssl._create_unverified_context #创建默认不认证的https文本
class TorrentClient:
    def __init__(
            self,
            torrent_source: Union[str, bytes],
            save_path: str,
            pt_params: dict = None,
            is_magnet: bool = False
    ):
        """
        初始化下载客户端
        :param torrent_source: 种子路径/磁力链接/二进制数据
        :param save_path: 文件保存目录
        :param pt_params: PT参数 {'passkey': 'xxx', 'trackers': []}
        :param is_magnet: 是否磁力链接
        """
        self.torrent_source = torrent_source
        self.save_path = save_path
        self.pt_params = pt_params or {}
        self.is_magnet = is_magnet
        self.handle = None  # 种子句柄
        self.metadata = None  # 磁力链接元数据

        # 初始化会话
        # self.session = lt.session()
        params = lt.session_params()
        print("libtorrent 版本:", lt.__version__)  # 必须输出 2.0.11.0
        # print(list(lt.default_settings()))
        # 配置 SSL 参数（2.0.x 正确方式）
        params.settings = {
            # 关闭证书验证（仅限调试，生产环境必须开启）
            'validate_https_trackers': False,  # 替代旧版 ssl_verify_hostname

            # 证书相关配置
            # 'ssl_ca_certificates': '',  # 替代旧版 ssl_ca_path
            # 'ssl_certificate': '/path/to/cert.pem',  # 客户端证书（如需双向认证）
            # 'ssl_private_key': '/path/to/key.pem'

        }

        # 创建会话并应用配置
        self.session = lt.session(params)

    def _is_private_tracker(self, tracker_url: str) -> bool:
        """判断是否为PT站点Tracker"""
        # 特征1：URL包含passkey参数
        # print(tracker_url)
        if 'passkey=' in tracker_url:
            return True

        # 特征2：已知PT站点域名匹配
        pt_domains = [
            r'\.privatehd\.',
            r'\.hdbits\.',
            r'\.passthepopcorn\.',
            r'\.broadcasthe\.net',
            r'\.empornium\.me',
        ]
        if any(re.search(d, tracker_url) for d in pt_domains):
            return True

        # 特征3：用户显式指定PT参数
        if self.pt_params.get('force_private', False):
            return True

        return False

    def _detect_pt_from_info(self, info: lt.torrent_info) -> bool:
        """直接从 torrent_info 检测 PT 类型"""
        # 特征1：种子标记为私有
        if info.priv():
            return True

        # 特征2：Tracker URL 包含 PT 标识
        for tracker in info.trackers():
            if self._is_private_tracker(tracker.url):
                return True

        # 特征3：用户强制指定
        return bool(self.pt_params.get('force_private', False))

    def _auto_detect_tracker_type(self) -> bool:
        """自动检测种子类型（PT返回True）"""
        if not self.handle:
            return False

        trackers = self.handle.torrent_file().trackers()
        return any(self._is_private_tracker(t.url) for t in trackers)

    def _configure_session(self , is_private: bool):
        """根据类型自动配置会话"""

        # lt.ssl_context(lt.ssl_context_t.disabled)
        params = lt.session_params()
        base_settings = {
            #'user_agent': 'Transmission/3.00',
            'user_agent': 'qBittorrent/4.6.4',  # 2024 年稳定版

            'handshake_client_version': 'qBittorrent 4.6.4',  # 握手时发送的版本
            'peer_fingerprint': 'qBittorrent 4.6.4',           # 客户端指纹
            #'peer_id_prefix': '-qB4640-',  # 4.6.4 的标识
            #'peer_fingerprint': 'Transmission/3.00',  # 部分 PT 站需要
            'listen_interfaces': '0.0.0.0:21415,[::]:21415',
            # 'outgoing_interfaces': 'eth1',#强制绑定网卡
            # 'announce_ip': self._get_external_ip(),  # 获取外网IP辅助NAT穿透
            # 'ssl_verify_peer': False,
        }

        print(base_settings)
        # 私有种子（PT）配置
        if is_private:
            print("[系统] 检测到 PT 种子，应用严格配置")
            base_settings.update({

                'peer_fingerprint': '-qB4640-',  # 控制 peer_id 前缀
                'user_agent': 'qBittorrent/4.6.4',  # HTTP Tracker 标识
                'handshake_client_version': 'qBittorrent 4.6.4',  # 握手协议标识

                'enable_dht': False,
                'enable_lsd': False,
                'enable_upnp': False,
                #'enable_natpmp': True,

                'allow_multiple_connections_per_ip': False,
                'announce_to_all_tiers': True,

                #'prefer_rc4': True,  # 部分 PT 站需要
                #'upnp_ignore_nonrouters': True  # 仅映射路由器
            })
            # 模仿 qBittorrent 的消息间隔
            base_settings.update({
                'tick_interval': 500,  # 默认 500ms
                'send_redundant_have': False,  # 禁用冗余消息
                'lazy_bitfields': True  # 延迟发送位图
            })

            # qBittorrent 默认队列设置
            base_settings.update({
                'max_out_request_queue': 300,
                'whole_pieces_threshold': 20,
                'request_queue_time': 3
            })

            # 匹配官方连接行为
            base_settings.update({
                'connections_limit': 500,
                'half_open_limit': 50,
                'unchoke_slots_limit': -1  # 自动管理
            })
        else:
            print("[系统] 检测到公开种子，启用 DHT 优化")
            base_settings.update({
                'enable_dht': True,
                'enable_lsd': True,
                'enable_upnp': True,
                'enable_natpmp': True,
                'upnp_ignore_nonrouters': True  # 仅映射路由器
            })

        params.settings = base_settings
        self.session.apply_settings(params.settings)

        pe = lt.pe_settings()
        pe.out_enc_policy = lt.enc_policy.forced
        pe.in_enc_policy = lt.enc_policy.forced

        pe.out_enc_policy = lt.enc_policy.enabled
        pe.in_enc_policy = lt.enc_policy.enabled
        pe.allowed_enc_level = lt.enc_level.both
        self.session.set_pe_settings(pe)
        # 添加必要扩展
        self.session.add_extension(lt.create_ut_pex_plugin)
        self.session.add_extension(lt.create_ut_metadata_plugin)


    def _process_magnet(self) -> lt.torrent_info:
        """处理磁力链接元数据获取"""
        # 添加磁力链接任务
        params = {
            "save_path": self.save_path,
            "storage_mode": lt.storage_mode_t.storage_mode_sparse
        }
        self.handle = lt.add_magnet_uri(self.session, self.torrent_source, params)

        print("[系统] 正在获取元数据，请稍候...")
        timeout = 300  # 5分钟超时
        start_time = time.time()

        # 等待元数据获取
        while not self.handle.has_metadata():
            if time.time() - start_time > timeout:
                raise RuntimeError("获取元数据超时，请检查网络连接")

            # 处理警报信息
            alerts = self.session.pop_alerts()
            for alert in alerts:
                if isinstance(alert, lt.metadata_received_alert):
                    print("[系统] 元数据获取成功")
                    return alert.handle.get_torrent_info()
            time.sleep(0.5)

        return self.handle.get_torrent_info()

    def _modify_tracker_urls(self, info: lt.torrent_info):
        """修改Tracker URL追加PT参数"""
        if 'passkey' not in self.pt_params:
            return

        passkey = self.pt_params['passkey']
        if len(passkey) != 32:
            print("[警告] passkey应为32位字符")
            return

        # 添加额外Tracker (磁力链接需要)
        if self.is_magnet and 'trackers' in self.pt_params:
            for tracker in self.pt_params['trackers']:
                info.add_tracker({'url': tracker, 'tier': 0})

        # 遍历所有Tracker追加passkey
        for tracker in info.trackers():
            parsed = urlparse(tracker.url)
            query = parse_qs(parsed.query)

            if 'passkey' in query:
                print(f"[检测] 发现现有passkey: {tracker.url}")
                continue

            # 构造新查询参数
            new_query = parsed.query
            if new_query:
                new_query += f"&passkey={passkey}"
            else:
                new_query = f"passkey={passkey}"

            # 重建URL
            tracker.url = urlunparse((
                parsed.scheme,
                parsed.netloc,
                parsed.path,
                parsed.params,
                new_query,
                parsed.fragment
            ))
            print(f"[更新] Tracker地址: {tracker.url}")

    def get_file_list(self) -> List[Dict]:
        """
        获取种子文件列表
        :return: 文件信息字典列表
        """
        if not self.handle or not self.handle.is_valid():
            raise RuntimeError("请先启动下载任务")

        torrent_info = self.handle.torrent_file()
        priorities = self.handle.file_priorities()

        return [
            {
                "index": idx,
                "path": file_entry.path,
                "size": file_entry.size,
                "priority": priorities[idx]
            }
            for idx, file_entry in enumerate(torrent_info.files())
        ]

    def print_file_list(self):
        """打印格式化文件列表"""
        files = self.get_file_list()
        print("\n【文件列表】")
        print(f"{'索引':<6}{'大小(MB)':<12}{'下载状态':<10}{'文件路径'}")
        print("-" * 60)
        for f in files:
            size_mb = f["size"] / 1024 / 1024
            # status = "√" if f["priority"] > 0 else "×"
            status = f["priority"]
            print(f"{f['index']:<6}{size_mb:.2f} MB{'':<4}{status:<10}{f['path']}")
        print()

    def _get_external_ip(self) -> str:
        """获取本机外网IP（用于NAT穿透）"""
        try:
            with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s:
                s.connect(("8.8.8.8", 80))
                return s.getsockname()[0]
        except Exception:
            return "0.0.0.0"  # 失败时回退


    def _get_global_ipv6(self) -> str:
        """获取有效的全局 IPv6 地址"""
        import subprocess
        result = subprocess.run(
            ["ip", "-6", "addr", "show", "scope", "global"],
            capture_output=True, text=True
        )
        for line in result.stdout.split('\n'):
            if 'inet6' in line and 'global' in line:
                return line.split()[1].split('/')[0]
        return ""

    def select_files(self, indices: List[int]):
        """
        选择要下载的文件
        :param indices: 文件索引列表 (0-based)
        """
        torrent_info = self.handle.torrent_file()
        total_files = torrent_info.num_files()

        # 校验索引有效性
        invalid = [i for i in indices if i < 0 or i >= total_files]
        if invalid:
            raise ValueError(f"无效文件索引: {invalid}")

        # 设置优先级 (0=不下载，7=最高)
        priorities = [0] * total_files
        for idx in indices:
            priorities[idx] = 7

        self.handle.prioritize_files(priorities)
        print(f"[操作] 已选择下载 {len(indices)} 个文件")

    def set_download_range(self, file_index: int, start_byte: int, end_byte: int):
        """
        设置文件下载范围
        :param file_index: 文件索引
        :param start_byte: 起始字节
        :param end_byte: 结束字节
        """
        if not 0 <= file_index < self.handle.torrent_file().num_files():
            raise ValueError("文件索引超出范围")

        # 计算全局偏移
        file_entry = self.handle.torrent_file().file_at(file_index)
        piece_size = self.handle.torrent_file().piece_length()
        global_start = file_entry.offset + start_byte
        global_end = file_entry.offset + end_byte

        # 计算涉及的分片
        start_piece = global_start // piece_size
        end_piece = (global_end - 1) // piece_size

        # 设置分片优先级
        piece_priorities = [0] * self.handle.torrent_file().num_pieces()
        for p in range(start_piece, end_piece + 1):
            piece_priorities[p] = 7  # 最高优先级

        self.handle.prioritize_pieces(piece_priorities)
        print(f"[操作] 文件 {file_index} 下载范围: {start_byte}-{end_byte} 字节")

    def prepare_download(self):
        """准备下载任务（直接解析种子文件）"""
        # 解析种子文件（无论磁力链还是种子文件）
        if self.is_magnet:
            info = self._process_magnet()
        else:
            # 直接通过二进制数据创建 torrent_info
            if isinstance(self.torrent_source, bytes):
                info = lt.torrent_info(self.torrent_source)
            else:
                with open(self.torrent_source, 'rb') as f:
                    info = lt.torrent_info(lt.bdecode(f.read()))

        # 提前执行 PT 检测（不依赖 self.handle）
        is_private = self._detect_pt_from_info(info)
        self._configure_session(is_private)  # 根据检测结果配置会话

        # 处理 PT 参数
        if self.pt_params:
            self._modify_tracker_urls(info)

        # 添加任务并暂停
        params = {
            "ti": info,
            "save_path": self.save_path,
            "storage_mode": lt.storage_mode_t.storage_mode_sparse,
            "flags": lt.torrent_flags.paused,
            #'peer_id':self.generate_qb_peer_id()  # 关键伪装点
        }
        self.handle = self.session.add_torrent(params)

        self.handle.set_upload_mode(True)
        self.handle.set_share_mode(True)

    def prepare_download2(self):
        """准备下载任务但不启动（用于获取文件列表）"""
        if self.is_magnet:
            info = self._process_magnet()
        else:
            info = lt.torrent_info(self.torrent_source)

        # PT参数处理
        if self.pt_params:
            self._modify_tracker_urls(info)

        # 添加任务并暂停
        params = {
            "ti": info,
            "save_path": self.save_path,
            "storage_mode": lt.storage_mode_t.storage_mode_sparse,
            "flags": lt.torrent_flags.paused  # 关键：添加时即暂停
        }
        self.handle = self.session.add_torrent(params)

        # PT必要设置
        self.handle.set_upload_mode(True)
        self.handle.set_sequential_download(False)

    def begin_download(self):
        """用户完成选择后开始下载"""
        self.handle.resume()  # 恢复下载
        # self.print_file_list() #打印文件列表
        self._monitor_progress()

    def _monitor_progress(self):
        """监控下载进度"""
        last_update = time.time()
        print("【操作提示】")
        print("1. 按 Ctrl+C 停止下载")
        print("2. 使用 select_files() 选择文件")
        print("3. 使用 set_download_range() 设置范围\n")

        try:
            while not self.handle.is_seed():
                status = self.handle.status()

                # 每秒更新显示
                if time.time() - last_update >= 1:
                    self._print_progress(status)
                    last_update = time.time()

                # 处理警报
                alerts = self.session.pop_alerts()
                for alert in alerts:
                    if isinstance(alert, lt.tracker_error_alert):
                        print(f"\n[Tracker错误] {alert.message()}")
                    elif isinstance(alert, lt.file_error_alert):
                        print(f"\n[文件错误] {alert.message()}")

                time.sleep(0.5)

            print("\n【下载完成】")

        except KeyboardInterrupt:
            print("\n【用户中断】正在保存进度...")
            self.handle.save_resume_data()
            print("进度已保存，可恢复下载")

    def _print_progress(self, status):
        """打印进度信息"""
        progress = status.progress * 100
        down_speed = status.download_rate / 1024
        up_speed = status.upload_rate / 1024
        peers = status.num_peers
        print(
            f"\r[进度] {progress:.1f}% | "
            f"↓{down_speed:.1f} KB/s | "
            f"↑{up_speed:.1f} KB/s | "
            f"连接数: {peers}",
            end=" ", flush=True
        )

    def generate_qb_peer_id(self):
        import random
        prefix = b"-qB4640-"  # qBittorrent 4.6.4 标识
        random_suffix = bytes([random.randint(0, 255) for _ in range(12)])
        return prefix + random_suffix


def main():
    """命令行入口"""
    if len(sys.argv) < 3:
        print("使用方法：")
        print("1. 种子文件下载：")
        print("   python pt_downloader.py <种子文件> <保存目录> [passkey]")
        print("2. 磁力链接下载：")
        print("   python pt_downloader.py <磁力链接> <保存目录> [passkey] --magnet")
        sys.exit(1)

    # 解析参数
    source = sys.argv[1]
    save_path = sys.argv[2]
    is_magnet = "--magnet" in sys.argv or source.startswith("magnet:")
    pt_params = {"passkey": sys.argv[3]} if len(sys.argv) > 3 else None

    try:
        client = TorrentClient(
            source,
            save_path,
            pt_params,
            is_magnet=is_magnet
        )
        client.start_download()

        # 示例：选择第一个文件的前100MB
        # client.select_files([0])
        # client.set_download_range(0, 0, 100*1024*1024)

    except Exception as e:
        print(f"\n[错误] {str(e)}")
        sys.exit(1)


if __name__ == "__main__":
    main()